package org.dslintegration.merge.core;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.dslintegration.history.EMetamodelSource;
import org.dslintegration.kM3.KM3Factory;
import org.dslintegration.kM3.impl.KM3FactoryImpl;
import org.dslintegration.mRules.MergeRules;
import org.dslintegration.mRules.Rule;
import org.dslintegration.mRules.RuleStatement;
import org.dslintegration.merge.InstanceLoader;
import org.dslintegration.merge.KM3MetamodelUtils;
import org.dslintegration.merge.core.exception.MergeException;
import org.dslintegration.merge.km3.converter.KM3Converter;
import org.dslintegration.merge.km3.core.Class;
import org.dslintegration.merge.km3.core.Classifier;
import org.dslintegration.merge.km3.core.DataType;
import org.dslintegration.merge.km3.core.Metamodel;
import org.dslintegration.merge.km3.core.Package;
import org.dslintegration.merge.km3.core.Reference;
import org.dslintegration.merge.km3.core.StructuralFeature;
import org.dslintegration.merge.km3.exporter.KM3MetamodelExporter;
import org.dslintegration.merge.simple.SimpleMerge;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.xmi.impl.EcoreResourceFactoryImpl;
import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl;
import org.eclipse.ui.console.MessageConsoleStream;

import freemarker.template.TemplateException;

public class MergeEngine {
	protected Metamodel left;
	protected Metamodel right;
	protected Metamodel out;
	protected MergeContext context;

	protected org.dslintegration.kM3.Metamodel ecoreOut;

	protected MergeRules rules;
	protected KM3Factory factory;
	protected MessageConsoleStream outputConsole;
	protected boolean success = true;

	public static void main(String[] args) throws Exception {
		MergeRules rules = InstanceLoader.loadRules(new File("C:/Projects/Master/org.dslintegration.MetaModelMerge/src/org/dslintegration/merge/Rules.mrs"));

		// MessageConsoleStream consoleStream = new MessageConsoleStream(new MessageConsole("Herp", null));
		MergeEngine me = new MergeEngine(rules, null);

		// File file = new File("c:/temp/generated");
		System.out.println(KM3MetamodelExporter.export(me.getOutput()));
		// me.exportAsKM3(file);
		// me.exportAsEcore(file);
		// me.exportAsXMI(file);

	}

	protected void consolePrint(String message) {
		// For fast testing purposes
		if (outputConsole != null) {
			outputConsole.println(message);
		} else {
			System.out.println(message);
		}
	}

	public MergeEngine(MergeRules rules, MessageConsoleStream outputConsole) throws IOException, MergeException {
		consolePrint("Merge Engine Initializing...");
		this.rules = rules;
		this.outputConsole = outputConsole;

		// Create the factory for metamodel creation
		factory = KM3FactoryImpl.init();
		// Load the metamodels from the rules
		consolePrint("Loading models from merge rule...");
		KM3Converter converter = new KM3Converter();
		try {

			converter.setMetamodelSource(EMetamodelSource.LEFT);
			left = converter.convert(InstanceLoader.loadMetamodel(new File(rules.getMetamodelPathLeft())));
			converter.setMetamodelSource(EMetamodelSource.RIGHT);
			right = converter.convert(InstanceLoader.loadMetamodel(new File(rules.getMetamodelPathRight())));

		} catch (IOException e) {
			e.printStackTrace();
			consolePrint("Error Loading Models : " + e.getMessage());
			success = false;
			return;
		} catch (Exception e) {
			e.printStackTrace();
			consolePrint("Error Loading Models : " + e.getMessage());
			success = false;
			return;
		}
		consolePrint("Loading models successfull...");
		// Create the merged metamodel
		out = new Metamodel("mm");

		out.contents.addAll(left.getContents());
		out.contents.addAll(right.getContents());

		context = new MergeContext(out);
		context.getHistory().append(converter.getHistory());
		consolePrint("Merging Models ...");
		doMerge();
		consolePrint("Polishing output...");
		doPolish();
		consolePrint("Converting output to EMF format...");
		doConvertOutput();
		// Dont print out history
		// System.out.println(context.getHistory());
	}

	protected void doMerge() throws MergeException {
		for (Rule rule : rules.getRules()) {
			consolePrint("MERGE:\t Merging for rule " + rule.getRuleName());
			// Firstly we need to define output package/class
			String source = rule.getSource().getQualifiedName();
			String target = rule.getTarget().getQualifiedName();

			// This is relative to the dominant parameter
			String output = "";
			if (rule.getOutput() != null) {
				// This is when the output is defined
				output = rule.getOutput().getQualifiedName();
			} else {
				// We must check which is dominant
				if (rule.getSource().isDominant()) {
					output = rule.getSource().getQualifiedName();
				}
				if (rule.getTarget().isDominant()) {
					output = rule.getSource().getQualifiedName();
				}
			}
			// Now we need to see if we are merging packages or classes
			// If there is hierarchical difference that means they are probably trying to merge classes with packages and such
			if (source.split("\\.").length != target.split("\\.").length || source.split("\\.").length != output.split("\\.").length) {
				throw new MergeException();
			}
			int type = source.split("\\.").length;// If it's 2 it's a class , if it's 1 it's a package
			if (type == 2) // MERGE ZE CLAZZ
				mergeClass(rule, source, target, output);
			if (type == 1) // Merge ZE PACKAGE NAO
				mergePackage(rule, source, target, output);
		}

	}

	protected void mergeClass(Rule rule, String source, String target, String output) throws MergeException {

		Class sourcec = KM3MetamodelUtils.findClass(source, context, false);
		Class targetc = KM3MetamodelUtils.findClass(target, context, false);
		Class outputc = KM3MetamodelUtils.findClass(output, context, false);
		// If SourceC or TargetC do not exist we should

		if (sourcec == null || targetc == null)
			throw new MergeException();

		if (outputc == null) {
			outputc = KM3MetamodelUtils.findClass(output, context, true);
		}

		if (outputc != sourcec)
			context.copyContents(sourcec, outputc, true);
		if (outputc != targetc)
			context.copyContents(targetc, outputc, true);
		
		//Set the super type of the new class
		outputc.setSuperTypeRef(sourcec.getSuperTypeRef());
		outputc.setSuperType(sourcec.getSuperType());

		for (RuleStatement statement : rule.getStatements()) {
			if (statement.getStatement().equals("Simple")) {
				SimpleMerge simple = new SimpleMerge();
				try {
					simple.merge(statement, context);
				} catch (MergeException e) {
					e.printStackTrace();
				}
			}
		}

		if (sourcec != outputc)
			context.changeType(sourcec, outputc, true);
		if (targetc != outputc)
			context.changeType(targetc, outputc, true);
	}

	protected void mergePackage(Rule rule, String source, String target, String output) throws MergeException {

		Package sourcePackage = KM3MetamodelUtils.findPackage(source, context, false);
		Package targetPackage = KM3MetamodelUtils.findPackage(target, context, false);
		Package outputPackage = KM3MetamodelUtils.findPackage(output, context, true);

		if (sourcePackage == null || targetPackage == null)
			throw new MergeException();

		// Now we need to see if there are specific rules or just copy everything
		if (rule.getStatements().isEmpty()) {
			if (sourcePackage != outputPackage) {
				context.copy(sourcePackage, outputPackage, true);
			}
			if (targetPackage != outputPackage) {
				context.copy(targetPackage, outputPackage, true);
			}

		}

	}

	/**
	 * Polish the mmetamodel , in this example case , just move the datatypes onto another primitive package
	 */
	protected void doPolish() {
		Set<String> types = new HashSet<String>();
		List<DataType> dataTypes = new ArrayList<DataType>();
		for (Classifier classifier : context.getClassifiers()) {
			if (classifier instanceof DataType) {
				types.add(classifier.getName());
				dataTypes.add((DataType) classifier);
			}
		}

		Package primitiveTypes = KM3MetamodelUtils.findPackage("PrimitiveTypes", context, true);
		// Now we need to find DataTypes in primitive types
		for (String type : types) {
			DataType dtype = new DataType(type);
			primitiveTypes.addContent(dtype);
			dtype.setParent(primitiveTypes);
			for (DataType olddt : dataTypes) {
				if (olddt.getName().equals(type)) {
					context.changeType(olddt, dtype, true);
				}
			}
		}

		// Polish the variables that might have the same name
		for (Classifier classifier : context.getClassifiers()) {
			if (classifier instanceof Class) {
				Class clazz = (Class) classifier;
				Set<String> fields = new HashSet<String>();
				Set<Reference> removal = new HashSet<Reference>();
				for (StructuralFeature f : clazz.getStructuralFeatures()) {
					if (f instanceof Reference) {
						Reference ref = (Reference) f;
						if(fields.contains(ref.getName())){
							removal.add(ref);
						} else {
							//ref.setName(ref.opposite.getName());
							fields.add(ref.getName());
							
						}
						
					}
				}
				//POLISH
				
				clazz.getStructuralFeatures().removeAll(removal);
			}
		}
	}

	/**
	 * Converts output to ecore model
	 */
	protected void doConvertOutput() {
		// out = context.getMetamodel();
		try {
			String stringMM = KM3MetamodelExporter.export(out);

			ecoreOut = InstanceLoader.loadMetamodel(new ByteArrayInputStream(stringMM.getBytes()));
		} catch (IOException e) {
			e.printStackTrace();
		} catch (TemplateException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Export as XMI File
	 * 
	 * @param mm
	 * @param file
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public void exportAsXMI(File file) throws FileNotFoundException, IOException {
		export(ecoreOut, file, "xmi", new XMIResourceFactoryImpl(), null);
	}

	public void exportAsEcore(File file) throws FileNotFoundException, IOException {
		export(ecoreOut, file, "ecore", new EcoreResourceFactoryImpl(), null);
	}

	public void exportAsKM3(File file) throws FileNotFoundException, IOException {
		if (!file.getAbsolutePath().endsWith(".km3")) {
			file = new File(file.getAbsolutePath() + ".km3");
			file.createNewFile();
		}
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(file);
			fos.write(KM3MetamodelExporter.export(out).getBytes());
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (fos != null)
				fos.close();
		}
	}

	protected void export(org.dslintegration.kM3.Metamodel mm, File file, String type, Object factoryImpl, Resource resource) throws FileNotFoundException, IOException {
		if (!file.getAbsolutePath().endsWith("." + type)) {
			file = new File(file.getAbsolutePath() + "." + type);
			file.createNewFile();
		}
		if (resource == null) {
			Resource.Factory.Registry reg = Resource.Factory.Registry.INSTANCE;
			Map<String, Object> m = reg.getExtensionToFactoryMap();
			m.put(type, factoryImpl);

			// Obtain a new resource set
			ResourceSet resSet = new ResourceSetImpl();
			// Create a resource
			resource = resSet.createResource(URI.createURI(type + ":/" + file.getName()));
			resource.getContents().add(mm);
		}
		// Get the first model element and cast it to the right type, in my
		// example everything is hierarchical included in this first node

		resource.save(new FileOutputStream(file), Collections.EMPTY_MAP);
	}

	public Metamodel getOutput() {
		return out;
	}

	public String getHistory() {
		return context.getHistory().toString();
	}

}
